Skip to content

fix(mint): close websocket when subscribed mint quotes expire unpaid#1036

Open
b-l-u-e wants to merge 1 commit into
cashubtc:mainfrom
b-l-u-e:fix/mint-websocket-quote-expiry-disconnect
Open

fix(mint): close websocket when subscribed mint quotes expire unpaid#1036
b-l-u-e wants to merge 1 commit into
cashubtc:mainfrom
b-l-u-e:fix/mint-websocket-quote-expiry-disconnect

Conversation

@b-l-u-e

@b-l-u-e b-l-u-e commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Inspired by the quote lifecycle work in #1022 (wallet-side handling of unpaid/expired invoices), this PR hardens the mint side: websocket subscriptions to bolt11_mint_quote are closed when all subscribed quotes are unpaid and past expiry, instead of holding connections open until mint_websocket_read_timeout (default 600s).

Also fixes mint quote expiry not being persisted or returned on GET: POST returned expiry, but GET and the websocket monitor saw null because mint_quotes had no expiry column and store_mint_quote never wrote it.

Problem

  • WebSocket clients subscribing to an unpaid mint quote could keep a connection open until the read timeout (600s) after the quote TTL, even though the quote is no longer actionable.
  • expiry was computed at quote creation but not stored in mint_quotes (unlike melt_quotes), so REST GET, MintQuote.from_row, and the expiry monitor could not see it.

Changes

  • MintQuote.from_row: load expiry from DB rows (SQLite + Postgres).
  • store_mint_quote: persist expiry on insert.
  • Migration m036_add_expiry_to_mint_quotes: add expiry column to mint_quotes.
  • LedgerEventClientManager: background poll (MINT_WEBSOCKET_QUOTE_EXPIRY_CHECK_INTERVAL, default 30s) closes WS with code 1000 / reason mint quote subscription expired when every subscribed mint quote is unpaid and expiry <= now.
  • Tests: websocket monitor unit tests + assert TTL quote survives round-trip via get_mint_quote.

Verification

Local mint:

MINT_QUOTE_TTL=60
FAKEWALLET_BRR=false
MINT_WEBSOCKET_QUOTE_EXPIRY_CHECK_INTERVAL=5
export MINT_URL=http://127.0.0.1:3338
QUOTE_RESP=$(curl -sS -X POST "$MINT_URL/v1/mint/quote/bolt11" \
  -H 'Content-Type: application/json' -d '{"amount": 64, "unit": "sat"}')
QUOTE_ID=$(echo "$QUOTE_RESP" | jq -r .quote)

POST and GET both return matching expiry:

{ "quote": "k4D70Q30zKmxgnAGmxGcGLYSdRNKL71TXj8ZRttb", "state": "UNPAID", "expiry": 1780484602 }

WebSocket (python3 /tmp/ws_quote_hold.py "$QUOTE_ID"), ~60s after quote creation:

CLOSED 1000 mint quote subscription expired

Mint log:

Closing websocket: all subscribed mint quotes expired unpaid

After expiry, GET still returns state: "UNPAID" with expiry set expected: state tracks payment/mint progress; expiry is a separate deadline. Clients treat UNPAID + expiry < now as dead; POST /v1/mint/bolt11 rejects with quote expired.

Test plan

 pytest tests/mint/test_mint_websocket_protocol.py

 pytest tests/mint/test_mint.py::test_mint_quote_ttl_setting_overrides_invoice_expiry

 Manual repro above (POST/GET expiry match, WS close at TTL + poll interval)

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 75.18%. Comparing base (0e9a2dc) to head (a0b6d9f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
cashu/mint/events/client.py 96.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1036      +/-   ##
==========================================
+ Coverage   75.13%   75.18%   +0.04%     
==========================================
  Files         110      110              
  Lines       12116    12152      +36     
==========================================
+ Hits         9103     9136      +33     
- Misses       3013     3016       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@b-l-u-e b-l-u-e force-pushed the fix/mint-websocket-quote-expiry-disconnect branch from 118a270 to 8b9af9c Compare June 3, 2026 14:34
Comment thread cashu/mint/events/client.py Outdated
Comment thread tests/mint/test_mint_websocket_protocol.py Outdated
@b-l-u-e b-l-u-e force-pushed the fix/mint-websocket-quote-expiry-disconnect branch from 8b9af9c to 48fdfd2 Compare June 10, 2026 05:50
Persist mint quote expiry in the database (migration + store path) and
poll bolt11_mint_quote subscriptions so idle websocket connections
are closed after all subscribed quotes are unpaid and past expiry.

Fixes MintQuote.from_row not loading expiry and store_mint_quote not
writing it (melt quotes already had expiry; mint quotes did not).
@b-l-u-e b-l-u-e force-pushed the fix/mint-websocket-quote-expiry-disconnect branch from 48fdfd2 to a0b6d9f Compare June 10, 2026 06:03
@b-l-u-e

b-l-u-e commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Thank you all for the feedback

made changes now we close WS once all subscribed mint quotes are terminal (PAID or UNPAID && expiry <= now).
Updated monitor logic + tests (including replacing the old “paid keeps open” case).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

3 participants